package uk.co.q3c.deplan.server.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;

import org.apache.log4j.Logger;

import com.googlecode.objectify.Key;

/**
 * Builds a listing of fields through reflection. The list is complete from the supplied object up through the class hierarchy, but omits
 * static and transient fields.<br>
 * <br>
 * Results are returned as a series of lists, mapped according to {@link ReflectionFieldBuilder#FieldTypes}
 * 
 * 
 * @author DSowerby 28 Oct 2008
 * 
 */
public class ReflectionFieldBuilder {
	private boolean excludeTransient = true;
	private boolean excludeStatic = true;
	transient protected final Logger logger = Logger.getLogger(getClass().getName());

	public enum FieldTypes {
		PRIMITIVE, STRING, COLLECTION, ARRAY, MAP, ENUM, GENERALOBJECT, UNDEFINEDOBJECT, REFERENCE, REFERENCE_LIST
	}

	@Deprecated
	public EnumMap<FieldTypes, List<Field>> buildFieldList(Object example) {

		// new map every time
		EnumMap<FieldTypes, List<Field>> map = new EnumMap<FieldTypes, List<Field>>(FieldTypes.class);

		// initialise the map
		for (FieldTypes fieldType : FieldTypes.values()) {
			ArrayList<Field> list = new ArrayList<Field>();
			map.put(fieldType, list);
		}

		// if null input return an empty map, but with correct structure
		if (example == null) {
			return map;
		}

		Class<? extends Object> clazz = example.getClass();
		while (clazz != null) {
			Field[] fields = clazz.getDeclaredFields();
			// this$0 is a reference to an outer class
			for (int i = 0; i < fields.length; i++) {
				boolean exclude = false;
				if (fields[i].getName().equals("this$0")) {
					exclude = true;
				}
				if (excludeStatic) {
					if (Modifier.isStatic(fields[i].getModifiers())) {
						exclude = true;
					}
				}

				if (excludeTransient) {
					if (Modifier.isTransient(fields[i].getModifiers())) {
						exclude = true;
					}
				}
				if (!exclude) {
					boolean allocated = false;
					Class<? extends Object> fieldType = fields[i].getType();

					if (fieldType.isPrimitive()) {
						map.get(FieldTypes.PRIMITIVE).add(fields[i]);
						allocated = true;
					}

					if (fieldType == String.class) {
						map.get(FieldTypes.STRING).add(fields[i]);
						allocated = true;

					}

					if (fieldType.isArray()) {
						map.get(FieldTypes.ARRAY).add(fields[i]);
						allocated = true;
					}

					if (fieldImplements(fieldType, Key.class)) {
						map.get(FieldTypes.REFERENCE).add(fields[i]);
						allocated = true;
					}

					if (fieldType.isEnum()) {
						map.get(FieldTypes.ENUM).add(fields[i]);
						allocated = true;
					}

					if (fieldImplements(fieldType, List.class)) {
						if (fields[i].getGenericType() == Key.class) {
							map.get(FieldTypes.REFERENCE_LIST).add(fields[i]);
							allocated = true;
						}

					}

					if (fieldImplements(fieldType, Collection.class)) {
						map.get(FieldTypes.COLLECTION).add(fields[i]);
						allocated = true;
					}
					if (fieldImplements(fieldType, Map.class)) {
						map.get(FieldTypes.MAP).add(fields[i]);
						allocated = true;
					}

					if (!allocated) {
						map.get(FieldTypes.GENERALOBJECT).add(fields[i]);
					}
				}

			}
			clazz = clazz.getSuperclass();
		}
		return map;
	}

	private boolean fieldImplements(Class<? extends Object> fieldType, Class<?> interfaceClass) {
		if (fieldType == interfaceClass) {
			return true;
		}
		Class<?> clazz = fieldType;
		while (clazz != null) {
			Class<?>[] intfs = clazz.getInterfaces();
			// ArrayList<Class<?>> list=new ArrayList<Class<?>>();
			for (int i = 0; i < intfs.length; i++) {
				if (intfs[i] == interfaceClass) {
					return true;
				}
			}
			clazz = clazz.getSuperclass();
		}
		return false;
	}

	/**
	 * Ignores/includes static and transient fields depending on the value of {@link #excludeStatic} and {@link #excludeTransient}. By
	 * default both are true. Returns list of fields and also populates validPropertyNames
	 * 
	 * @param example
	 * @return
	 * @throws ClassNotFoundException
	 */
	public Map<String, Field> buildOrderedFieldList(Object example, List<String> validPropertyNames) throws ClassNotFoundException {

		Map<String, Field> fieldMap = new TreeMap<String, Field>();
		if (example == null) {
			logger.error("Cannot build field list from a null example, returning empty list");
			return fieldMap;
		}
		Class<? extends Object> clazz = example.getClass();

		// create a stack with the target object at the bottom
		// so that hierarchy builds from "highest" superclass

		Stack<Class<? extends Object>> stack = new Stack<Class<? extends Object>>();
		while (clazz != null) {
			stack.push(clazz);
			clazz = clazz.getSuperclass();
		}

		// work through the stack, adding fields to the field list
		// but ignoring transient fields
		while (stack.size() > 0) {
			clazz = stack.pop();
			Field[] fields = clazz.getDeclaredFields();
			// this$0 is a reference to an outer class
			for (int i = 0; i < fields.length; i++) {
				boolean exclude = false;
				if (fields[i].getName().equals("this$0")) {
					exclude = true;
				}
				if (excludeStatic) {
					if (Modifier.isStatic(fields[i].getModifiers())) {
						exclude = true;
					}
				}
				if (excludeTransient) {
					if (Modifier.isTransient(fields[i].getModifiers())) {
						exclude = true;
					}
				}

				if (!exclude) {
					fieldMap.put(fields[i].getName(), fields[i]);

					if (validPropertyNames != null) {
						validPropertyNames.add(fields[i].getName());
					}
				}
			}
		}

		return fieldMap;
	}

	public boolean isExcludeTransient() {
		return excludeTransient;
	}

	public void setExcludeTransient(boolean excludeTransient) {
		this.excludeTransient = excludeTransient;
	}

	public boolean isExcludeStatic() {
		return excludeStatic;
	}

	public void setExcludeStatic(boolean excludeStatic) {
		this.excludeStatic = excludeStatic;
	}
}
